Hauptseite Zu yanwittmann.de

Zur Hauptseite

    4. Interrupts / Polling    

Hardware >> Microcontroller


Bei Interrupts und Polling handelt es sich um zwei verschiedene Möglichkeiten, die unterschiedlichsten Ereignisse (Timer, externe Ereignisse) im Microcontroller zu registrieren und zu verarbeiten. Es folgt eine kurze Beschreibung der beiden Methoden, Interrupts werden jedoch genauer beschrieben.
Hier schnell der Unterschied zwischen Interrupt und Polling in einer Grafik:


Polling

Polling überprüft direkt in der Hauptschleife des Programms, ob ein bestimmtes Ereignis eingetreten ist. Also: an einer Stelle im Programm ist eine Abfrage implementiert, welche überprüft, ob das Ereignis aufgetreten ist. Sollte dies der Fall sein, so wird dafür vorgesehener Code ausgeführt, der normalerweise mit einem rjmp übersprungen worden werde. Dadurch, dass die Abfrage beim Polling direkt in der Hauptschleife des Programms implementiert ist, wird dementsprechend auch nur einmal pro Schleifendurchlauf überprüft, ob das Ereignis eingetreten ist, was zu einer verzögerten Reaktion führt. Durch die Überprüfung, ob das Ereignis eingetreten ist, wird außerdem CPU-Zeit verwendet, was zu weiteren Verzögerungen für das restliche Programm führt.


Interrupts

Interrupts sind Programmstränge die parallel zum normalen Programm laufen und damit unabhängig vom Status der Hauptschleife sind. Sobald ein Ereignis auftritt, wird sofort der Interrupt ausgelöst und behandelt, der normale Programmablauf wird hierfür an egal welcher Stelle unterbrochen.

Funktionsweise von Interrupts:
Jeder Art von Interrupt ist eine Adresse im Programmspeicher zugeordnet. Wird ein Interrupt ausgelöst, springt der Programmzeiger auf diese dem Interrupt zugewiesenen Adresse. Diese Adresse befindet sich in der sogenannten Interruptvektortabelle, welche sich fast ganz am Anfang des Programmspeichers befindet.
Jeder Eintrag in der Interruptvektortabelle ist nur eine einzige Adresse lang. Darum muss in dieser ein rjmp Befehl stehen, der dann zur eigentlichen auszuführenden Stelle im Programmspeicher springt.


Im der obigen Grafik ist eine solche Vektortabelle zu sehen. Die importierte Konstante INT_VECTORS_SIZE enthält hierbei die Adresse hinter der Vektortabelle.
Da nicht immer alle Interrupts benötigt werden, müssen nur die Interrupts angelegt werden, die später auch verwendet werden sollen. Der im Beispiel (Grafik oben) dargestellte Interrupt INT0 (externer Interrupt 0) wird wie folgt erstellt:

.include "tn2313def.inc"          
.org INT0addr
rjmp isrINT0
Assembler

Mit .include "tn2313def.inc" wird eine Microcontroller-spezifische Datei geladen, welche Konstanten enthält, die im darauf folgenden Code verwendet werden können (z.B. INT_VECTORS_SIZE, INT0addr oder ISC01). Der nächste Befehl .org INT0addr ist dafür zuständig, dass der Assembler als nächstes in die Adresse INT0addr schreibt. Hierbei ist es wichtig zu verstehen, dass es sich wirklich um eine Assembler-Direktive handelt, diese Zeile also am Ende nicht im Programmspeicher vorhanden sein wird.
Da vor dem dritten Befehl kein Punkt steht, dieser also keine Speicher-Direktive ist, wird die Assembler-Anweisung tatsächlich in Maschinensprache umgewandelt und in den Programmspeicher geschrieben. Bei der Adresse INT0addr steht nun also rjmp isrINT0, nur eben in Maschinencode übersetzt.

Falls jetzt das Interrupt INT0 ausgelöst wird, springt der Programmzeiger direkt zur Adresse vom Interrupt 0: INT0addr. Der Befehl, der in diesem Feld steht, wird dementsprechend anschließend ausgeführt. Falls dort jedoch, sei es aus Unachtsamkeit des Programmierers, kein Befehl stehen sollte, geht der Programmzeiger einfach Zeile für Zeile nach unten und führt die Befehle aus, auf die er stößt. Im Normalfall steht dort jedoch eine rjmp-Anweisung (hier: rjmp isrINT0), mit der dann in die eigentliche Interruptbehandlung, zur sogenannten ISR, (Interrupt Service Routine) gesprungen wird.

ISR
Eine Interrupt service routine beginnt eigendlich immer mit einem Marker, zu dem gesprungen wird, und endet immer mit dem Befehl reti, der wieder zur ursprünglichen Adresse im Programmspeicher springt, die vor der Interruptbehandlung zuletzt ausgeführt wurde. Zwischen diesen beiden Befehlen befinden sich normale Befehle.

isrINT0:  ; Marker zu dem aus der Interruptvektortabelle gesprungen wird          
  ; weitere Befehle in der ISR
reti      ; zur Tabelle zurückspringen
Assembler

Durch reti wird der Interrupt also beendet. Steht am Ende der ISR kein reti, springt der Programmzeiger nicht zurück in den Programmcode und führt so lange die Befehle aus, auf die er stößt, bis er am Ende des Programmspeichers angelangt ist oder auf ein anderes reti, womöglich von einer anderen ISR, stößt.

Vor Ausführung der eigentlichen Befehle in der ISR sollten wichtige Inhalte aus Registern (unter anderem das Statusregister SREG) auf dem Stack (Stapelspeicher) gespeichert werden, sodass diese Registerinhalte durch die Ausführung der ISR überschrieben und danach wiederhergestellt werden können. Auf diese Weise gehen keine Daten des Hauptprogramms verloren und die ISR kann die Register so verwenden, wie sie es will. So werden auch zufällig erscheinende Probleme vermieden. Der Stack beginnt am Ende des SRAMs und wächst dann Richtung Anfang, man setzt daher den Stackpointer zu Beginn auf die letzte Speicherzelle: es wird also zunächst die Adresse des Ende des RAMs gesucht. Da die bei uns verwendeten Microcontroller 16 Bit-Adressen verwenden, setzt sich die Adresse aus einem oberen und einem unteren Bit zusammen, die je gesichert werden müssen.
Das wird wie folgt getan:

ldi r16, LOW(RAMEND)   ; untere 8 Bit werden in r16 geladen          
out SPL, r16           ; speichern der Position der unteren 8 Bit          
ldi r16, HIGH(RAMEND)  ; das gleiche für die oberen 8 Bit
out SPH, r16
Assembler

Somit befindet sich nun die Adresse in SPL und SPH. Für gewöhnlich findet das holen dieser Adressen ein mal am Anfang des Codes statt (und nicht in der Hauptschleife).

Das eigentliche Sichern der Register auf dem Stack erfolgt dann in den jeweiligen ISRs:

push r16        ; sichern des Registers r16          
in   r16, SREG  ; das Statusregister wird in r16 geladen          
push r16        ; und wird ebenfalls gesichert
  ; ... Befehle der Interruptbehandlung
pop  r16        ; das Statusregister wird wieder runtergeholt          
out  SREG, r16  ; und zurückgeschrieben
pop  r16        ; r16 wird wiederhergestellt
Assembler

Was passiert hier?
Der Befehl push r16 schiebt zuerst den inhalt von r16 auf den Stack. Anschließend wird mit in r16, SREG das Statusregister in r16 geladen. Dieses wird dann wieder mit push r16 auf den Stack geschoben.
Es folgen die eigentlichen Befehle der Interruptbehandlung. Um die gesicherten Daten der Register wieder aus dem Stack in die eigentlichen Variablen zu verschieben muss man zuerst das Register r16 mit dem SREG vom Stapel holen, da dieses ganz oben liegt (mit dem Befehl pop r16). Dann speichert man den Wert wieder in SREG mit in r16, SREG. Anschließend holt man noch das eigentliche Register r16 aus dem Stack mit pop r16.

Hier ist der Ablauf eines solchen Interrupts (hier für das eben implementierte INT0) einmal zu sehen:

Nun haben wir für unseren Fall die ISR fertig implementiert.


Allgemein Interrupts initialisieren

Allgemein sind folgende Schritte notwendig, um ein Interrupt zu initialisieren:


Es gibt verschiedene Arten von Interrupts, die alle bei unterschiedlichen Dingen auslösen. Die für uns relevanten sind Externe Interrupts und Timer-Interrupts.